package org.chipu.jnids.sancp;

import java.io.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Level;
import org.chipu.jnids.beans.Flow;
import org.chipu.jnids.display.TextConsole;
import org.chipu.jnids.sce.profile.DateAsNumber;
import org.chipu.shared.util.SoftHashMap;
import org.chipu.shared.util.singletons.LoggingUtils;
import org.chipu.shared.util.singletons.NetworkUtils;

/** Read data from SANCP files. */
public class NetFlowReader {
	private static NetFlowReader INSTANCE;
	private static final Map<String, Class<?>> types = new HashMap<String, Class<?>>();
	private static final SimpleDateFormat timeFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
	static {
		fillTypesManually();
	}
	private final File configFile;

	private NetFlowReader(String sancpConfFile) {
		configFile = new File(sancpConfFile);
	}

	public static NetFlowReader load(String sancpConfFile) {
		INSTANCE = new NetFlowReader(sancpConfFile);
		return INSTANCE;
	}

	public static NetFlowReader getInstance() {
		if (INSTANCE == null)
			throw new IllegalStateException("NetFlowReader controller not initialized");
		return INSTANCE;
	}

	public Map<String, Class<?>> getTypes() {
		return types;
	}

	private static void fillTypes() {
		try {
			fillTypesAutomatically();
		} catch(IOException e) {
			e.printStackTrace();
			fillTypesManually();
		}
	}

	private static void fillTypesManually() {
		types.put("sancp_id", Long.class);
		// types.put("start time", Integer.class);
		// types.put("end time", Integer.class);
		// types.put("erased time", Short.class);
		// types.put("hw_proto", Long.class);
		// types.put("source address", Integer.class);
		// types.put("source port", Short.class);
		// types.put("destination address", Integer.class);
		// types.put("destination port", Short.class);
		types.put("start_time_gmt", Date.class);
		types.put("stop_time_gmt", Date.class);
		types.put("duration", Integer.class);// or Long
		types.put("ip_proto", Long.class);
		types.put("src_ip_decimal", Long.class);// long or integer without sign
		types.put("dst_ip_decimal", Long.class);// long or integer without sign
		types.put("src_ip_dotted", String.class);
		types.put("dst_ip_dotted", String.class);
		types.put("src_port", Integer.class);// integer or short without sign
		types.put("dst_port", Integer.class);// integer or short without sign
		types.put("src_pkts", Long.class);
		types.put("src_bytes", Long.class);
		types.put("dst_pkts", Long.class);
		types.put("dst_bytes", Long.class);
		types.put("sflags", Short.class);// short or byte without sign
		types.put("dflags", Short.class);// short or byte without sign
	}

	private static void fillTypesAutomatically() throws IOException {
		BufferedReader d = null;
		try {
			String unixCommand = "sancp -h";
			Process p = Runtime.getRuntime().exec(unixCommand);
			d = new BufferedReader(new InputStreamReader(new DataInputStream(p.getInputStream())));
			String tmp = "";
			while(tmp != null) {
				if (tmp != "")
					if (tmp.matches("^[\\s]*[\\d]*:.*")) {
						String[] tokens = tmp.split("\\s");
						String type = tokens[getIndexOfNonEmptyItem(tokens, 0, 1)];
						if (canRecognizeClass(type))
							types.put("", getNecessaryClass(type));// TODOLATER
					}
				tmp = d.readLine();
			}
		} finally {
			if (d != null)
				d.close();
		}
	}

	/**
	 * @param occurrency the first occurrency corresponds to 0.
	 */
	private static int getIndexOfNonEmptyItem(String[] items, int offset, int occurrency) {
		int rta = -1;
		for(int i = offset; i < items.length; i++)
			if (items[i] != null && !"".equals(items[i]))
				rta = i;
		if (occurrency == 0)
			return rta;
		return getIndexOfNonEmptyItem(items, rta + 1, occurrency - 1);
	}

	private static boolean canRecognizeClass(String s) {
		return getNecessaryClass(s) != null;
	}

	private static Class<?> getNecessaryClass(String s) {
		if ("8bit".equals(s))
			return Byte.class;
		if ("16bit".equals(s))
			return Short.class;
		if ("32bit".equals(s))
			return Integer.class;
		if ("64bit".equals(s))
			return Long.class;
		if ("Y/N".equals(s))
			return Boolean.class;
		if ("17byte".equals(s))
			return String.class;
		return null;
	}

	private String[] getPattern(String keyWord) throws IOException {
		if (!configFile.exists() || !configFile.canRead())
			throw new IOException("Cannot read config file '" + configFile.getAbsolutePath() + "'. Exists?"
				+ configFile.exists() + ". Is readable?" + configFile.canRead());
		// the pipes does not work as expected
		// String unixCommand = "cat " + configFile.getAbsolutePath()
		// + " | grep \"format " + keyWord + "\"";
		// Process p = Runtime.getRuntime().exec(unixCommand);
		// BufferedReader d = new BufferedReader(new InputStreamReader(
		// new DataInputStream(p.getInputStream())));
		BufferedReader d = null;
		try {
			d = new BufferedReader(new FileReader(configFile));
			String formatLine = null;
			String tmp = "";
			while(tmp != null) {
				if (tmp.startsWith("format " + keyWord))
					formatLine = tmp;
				tmp = d.readLine();
			}
			if (formatLine == null) {
				if ("stats".equals(keyWord))
					formatLine = "format stats sancp_id,start_time_gmt,stop_time_gmt,erased_time_gmt,eth_proto,ip_proto,src_ip_decimal,src_port,dst_ip_decimal,dst_port,duration,timeout,src_pkts,dst_pkts,src_bytes,dst_bytes,sflags dflags,cflags,total_bytes,collect,collected,climit,tcplag,pcap,realtime,stats,reversed,hash,rid,rgid,node,zone,status,retro,src_mac,dst_mac";
				else if ("realtime".equals(keyWord))
					formatLine = "format realtime start_time_gmt,duration,src_ip_dotted,dst_ip_dotted,ip_proto,src_port,dst_port,src_pkts,dst_pkts,src_bytes,dst_bytes,total_bytes,collect,climit,collected,pcap,realtime,stats,retro,src_mac,dst_mac";
				else
					throw new IllegalArgumentException("The keyword is not valid: " + keyWord);
				LoggingUtils.getLogger().warning(
					"Format " + keyWord + " not found in file " + configFile + ". Using default value:\n\t"
						+ formatLine);
			}
			return formatLine.substring(("format " + keyWord).length()).trim().split(",");
		} finally {
			if (d != null)
				d.close();
		}
	}

	/** @return a list of the flows of a SANCP file */
	public List<Flow> retrieveFlows(File logFile, String patternKey) throws Exception {
		FlowConstructor helper = new FlowConstructor(logFile);
		processFile(patternKey, helper, logFile);
		return helper.getFlows();
	}

	void processFile(String patternKey, LogFileProcessor filler, File... logFiles) throws Exception {
		String[] pattern = getLineFormat(patternKey);
		filler.startFilling(pattern, getTypes());
		try {
			for(File f : logFiles) {
				BufferedReader d = null;
				try {
					filler.nextFile(f);
					d = new BufferedReader(new FileReader(f));
					String tmp = "";
					while(tmp != null) {
						if (tmp != "")
							filler.fill(getLineData(tmp, pattern));
						tmp = d.readLine();
					}
					d.close();
				} catch(Exception ex) {
					LoggingUtils.getLogger().log(Level.WARNING,
						"Error processing file: " + f + "\n\twith pattern: " + Arrays.toString(pattern), ex);
					throw ex;
				} finally {
					if (d != null)
						d.close();
				}
			}
		} finally {
			filler.endFilling();
		}
	}

	public Map<String, Number> getLineData(String line, String patternKey) throws IllegalArgumentException {
		return getLineData(line, getLineFormat(patternKey));
	}

	private Map<String, Number> getLineData(String line, String... pattern) throws IllegalArgumentException {
		String[] values = line.split("\\|");
		if (values.length != pattern.length)
			throw new IllegalArgumentException("Pattern line non valid.\n\tPattern (" + pattern.length
				+ "): " + Arrays.toString(pattern) + "\n\tData (" + values.length + "): "
				+ Arrays.toString(values));
		Map<String, Number> map = new HashMap<String, Number>();
		for(int i = 0; i < pattern.length; i++)
			try {
				map.put(pattern[i], getValue(pattern[i], values[i]));
			} catch(ParseException e) {
				e.printStackTrace();
			}
		return map;
	}

	private static Number getValue(String name, String value) throws ParseException {
		Class<?> clas = types.get(name);
		if (clas == null || name == null)
			return null;
		if (clas == Byte.class)
			return Byte.parseByte(value);
		if (clas == Short.class)
			return Short.parseShort(value);
		if (clas == Integer.class)
			return Integer.parseInt(value);
		if (clas == Long.class)
			return Long.parseLong(value);
		if (clas == Date.class)
			return new DateAsNumber((int) (timeFormat.parse(value).getTime() / 1000));
		if (name.equals("src_ip_dotted") || name.equals("dst_ip_dotted"))
			return NetworkUtils.getIp(value);
		return null;
	}

	/** Helps to construct a list of the flows based on a SANCP file */
	private static class FlowConstructor implements LogFileProcessor {
		private List<Flow> connections;
		private final String s;

		public FlowConstructor(File logFile) {
			s = logFile == null? null: logFile.getName();
		}

		@Override
		public void startFilling(String[] pattern, Map<String, Class<?>> types) {
			TextConsole.getInstance().startProgress("FlowConstructor",
				"Starting building flows" + (s == null? "": " (" + s + ")"));
			connections = new ArrayList<Flow>();
		}

		@Override
		public void fill(Map<String, Number> data) {
			connections.add(convertToFlow(data));
			// logger.log(l, "Package data: " + data);
			TextConsole.getInstance().doProgress("FlowConstructor");
		}

		@Override
		public void nextFile(File f) {/**/}

		@Override
		public void endFilling() {
			TextConsole.getInstance().endProgress("FlowConstructor",
				". \n\t" + connections == null? "-": connections.size() + " flows built");
		}

		public static Flow convertToFlow(Map<String, Number> data) {
			// return new Flow(data.get("ip_proto").toString(), data.get("start_time_gmt"),
			// data.get("stop_time_gmt"), data.get("src_ip_decimal"), data.get("dst_ip_decimal"),
			// data.get("src_port"), data.get("dst_port"));
			return new Flow(data.get("ip_proto"), data.get("start_time_gmt"), getDuration(data),
				getSrcIp(data), getDstIp(data), data.get("src_port"), data.get("dst_port"));
		}

		/** In seconds */
		private static Number getDuration(Map<String, Number> data) {
			Number n = data.get("duration");
			if (n == null)
				n = (int) (((Long) data.get("stop_time_gmt") - (Long) data.get("start_time_gmt")) / 1000);
			return n;
		}

		private static String getSrcIp(Map<String, Number> data) {
			return getIp(data, "src_");
		}

		private static String getDstIp(Map<String, Number> data) {
			return getIp(data, "dst_");
		}

		private static String getIp(Map<String, Number> data, String prefix) {
			Number n = data.get(prefix + "ip_dotted");
			if (n == null)
				n = data.get(prefix + "ip_decimal");
			if (n == null)
				return "";
			return NetworkUtils.getIp((Long) n);
		}

		public List<Flow> getFlows() {
			return connections;
		}
	}

	// -----
	private final Map<String, String[]> formatCache = Collections
		.synchronizedMap(new SoftHashMap<String, String[]>());

	private String[] getLineFormat(String patternKey) {
		assert "stats".equals(patternKey) || "realtime".equals(patternKey);
		if (formatCache.containsKey(patternKey))
			return formatCache.get(patternKey);
		try {
			String[] s = getPattern(patternKey);
			formatCache.put(patternKey, s);
			return s;
		} catch(IOException ex) {
			LoggingUtils.getLogger().log(Level.SEVERE, "Could not retrieve SANCP " + patternKey + " format",
				ex);
			return new String[0];
		}
	}

	public static String[] getStatLineFormat() {
		return getInstance().getLineFormat("stats");
	}

	public static String[] getRealtimeLineFormat() {
		return getInstance().getLineFormat("realtime");
	}
}